Five Lines of Code
도서 링크: https://www.yes24.com/Product/Goods/116904325
3장. 긴 코드 조각 내기
DRY 및 KISS 지침들이 어려웠던 이유
- 메서드가 여러가지 다른 일을 수행한다.
- 낮은 수준의 원시 연산을 사용한다.
- 주석과 적절한 메서드와 변수명 같이 사람이 읽을 수 있는 텍스트가 부족하다.
왜 다섯 줄인가?
정의: 메서드는 {
와 }
를 제외하고 5줄 이상이 되어서는 안 된다.
모든 메서드를 이 규칙을 준수하도록 바꿀 수 있다.
스멜
메서드가 길다는 것 자체가 스멜이다. 긴 메서드의 모든 논리를 머릿속에 담아야해서 작업하기가 어렵다.
의도
시간이 지남에 따라 추가되는 기능들로 인해 메서드가 커지는 경향이 있다. 5줄의 코드가 있는 4개의 메서드가 20줄인 하나의 메서드보다 훨씬 빠르고 이해하기 쉽다.
코드를 이해하기 위한 첫 번째 단계는 항상 함수명을 고려하는 것
리펙터링: 메서드 추출
메서드를 더 작은 조각으로 분해하는 리팩터링 기법이다.
- 원하는 이름으로 새로운 빈 메서드 생성
- 그룹의 맨 위에서 새로운 메서드 호출
- 그룹의 코드를 잘라내어 새로운 메서드의 본문에 붙여 넣기
- 컴파일
- 메개변수를 도입하여 호출하는 쪽의 오류를 발생시키기
- 매개변수 중 하나를 반환값으로 할당해야 할 경우:
- 새로운 메서드의 마지막에 반환값을 추가
- 새로운 메서드를 호출하는 쪽에서 반환값을 할당
- 컴파일
- 호출쪽 인자 셋팅
- 사용하지 않는 코드와 주석 제거
이 절차는 아무것도 손상시키지 않는다. 코드가 수행하는 작업을 아직 살펴보지 않는 경우에는 아무것도 고장내지 않았다는 확신이 가장 중요함
규칙: 호출 또는 전달, 한 가지만 할 것
함수 내에서는 객체에 있는 메서드를 호출하거나 객체를 인자로 전달할 수 있지만 둘을 섞어 사용해서는 안된다.
많은 메서드 및 여러 가지 매개변수를 전달하는 경우, 결국 해당 함수의 책임이 고르지 않게 될 수 있다.
function average(arr: number[]) {return sum(arr) / arr.length}function average(arr: number[]) {return sum(arr) / size(arr)}
위 함수는 높은 수준의 추상화와 낮은 수준의 arr.length 를 모두 사용
스멜
함수의 내용은 동일한 추상화 수준에 있어야 한다. 전달된 인자의 메서드가 어떻게 사용되는지 식별하는 방ㄹ법은 인자로 전달된 변수 옆의 .
을 통해 찾을 수 있다.
의도
메서드에서 몇 가지 세부적인 부분을 추출해서 추상화를 도입할 때 연관된 다른 세부적인 부분도 추출하게 한다. 이러면 메서드 내부의 추상화 수준이 항상 동일하게 유지
함수의 내용은 동일한 추상화 수준에 있어야 한다.
좋은 함수의 이름의 속성
- 정직해야 한다. 함수의 의도를 설명해야 한다.
- 완전해야 한다. 함수가 하는 모든 것을 담아야 한다.
- 도메인에서 일하는 사람이 이해할 수 있어야 한다.
함수명을 지을 때는 항상 나중에 함수가 더 작아졌을 때 이름을 개선할 수 있는지를 평가해 봐야한다.
규칙: if 문은 함수의 시작에만 배치
if 문이 있는 경우 해당 if 문은 함수의 첫번째 항목이어야 한다.
함수는 한 가지 일만 해야 한다. 무언가를 확인하는 것은 한가지 일 즉 if 가 있는 경우 함수의 첫번째 항목이어야 하고, 그 후에는 아무것도 해서는 안된다.
if 문을 분리하가 위해 메서드 추출을 사용한다.
function reportPrimes(n: number) {for (let i = 2; i < n; i++) {if (isPrime(i)) {console.log(`${i} is prime`);}}}function reportPrimes(n: number) {for (let i = 2; i < n; i++) {reportIfPrime(i);}}function reportIfPrime(n: number) {if (isPrime(i)) {console.log(`${i} is prime`);}}
스멜
다섯 줄 제한과 같이 이 규칙은 함수가 한가지 이상의 작업을 수행하는 스멜을 막기 위해 존재
의도
if 문이 하나의 작업이기 때문에 이를 분리할 때 이어지는 else if 는 if 문과 분리할 수 없는 원자 단위로 봄
4장. 타입 코드 처리하기
규칙: if 문에서 else 를 사용하지 말 것
프로그램에서 이해하지 못하는 타입인지를 검사하지 않는 한 if 문에서 else 를 사용하지 않는다.
if-else
를 사용하면 코드에서 결정이 내려지는 지점을 고정하게 된다.
독립된 if 문은 검사로 간주하고, if-else 문은 의사 결정으로 간주한다.
// AS ISfunction average(ar: number[]) {if (size(ar) === 0) {throw new Error("Cannot take average of empty array");}return sum(ar) / size(ar);}// TO BEfunction average(ar: number[]) {assertNotEmpty(ar);return sum(ar) / size(ar);}function assertNotEmpty(ar: number[]) {if (size(ar) === 0) {throw new Error("Cannot take average of empty array");}}
스멜
이른 바인딩: if-else 같은 의사결정 동작은 컴파일 시 처리되어 애플리케이션에 고정되면 재컴파일 없이는 수정할 수 없다.
늦은 바인딩: 코드가 실행되는 순간 동작이 결정됨
의도
if 는 흐름을 제어 -> 다음에 실행할 코드를 결정한다는 뜻
리펙터링: 클래스로 타입 코드 대체
열거형을 인터페이스로 변환하고 열거형의 값들을 클래스가 되도록 변환한다.
해당 리펙터링 패턴은 자체적으로 많은 가치를 가지지 않지만, 추후 개선을 가능하기 한다.
리펙터링: 클래스로 코드 이관하기
기능을 클래스로 옮기면서 클래스로 타입 코드 대체 패턴의 자연스러운 연장선
특정 값과 연결된 기능이 값에 해당하는 클래스로 이동하기 때문에 불변속성을 지역화
리펙터링: 메서드의 인라인화
프로그앰에서 더 이상 가독성에 도움이 되지 않는 메서드를 제거
단, 메서드를 인라인으,로 사용하는 것과는 다른 개념
메서드가 인라인화 하기 너무 복잡한가 -> 낮은 수준의 연산에 의존하여 가독성에 도움이 되는 경우에는 인라인화하지 않는다.
리펙터링: 메서드 전문화
일반화하고 재사용하려는 본능적인 욕구가 있지만 그렇게 하면 책임이 흐려지고, 다양한 위치에서 코드를 호출할 수 있기 때문에 문제가 될 수 있음
전문화된 메서드는 더 적은 위치에서 호출되어 필요성이 없어지면 더 쉽게 제거 가능함
규칙: switch 를 사용하지 말것
default 케이스가 없고 모든 case 에 반환값이 존재하는 경우에는 switch 를 사용하지 않는다.
switch 의 문제점
- 컴파일러 입장에서 새로 추가한값의 처리에 대한 누락을 알 수 없다.
- break 키워드 누락으로 인한 버그
스멜
switch 문은 값을 처리하는 방법에 초점, 클래스에 기능을 추가할때는 값이 상황을 처리하는 방법에 초점
컨텍스트에 초점을 맞춘다는 것은 불변속성을 전역화하는 것을 의미
의도
switch -> else if -> 클래스로 변환
인터페이스 대신 추상 클래스를 사용할 수 없을까?
사용할 수 있다. 코드의 중복을 피할 수 있다.
하지만 인터페이스를 사용하면 개발자가 능동적으로 무엇인가를 해야함. 즉, 누락으로 인한 오류를 방지할 수 있음
규칙: 인터페이스에서만 상속받을 것
상속은 오직 인터페이스를 통해서만 받는다.
추상 클래스를 사용하는 것은 일부 메서드의 기본 구현을 제공하고, 다른 메서드를 추상화 하기 위함 -> 중복을 줄이고 코드의 줄을 줄이고자 할 경우 편리
하지만 추상 클래스의 코드 공유는 커플링을 유발하고, 기본 구현으로 인해 재정의가 필요한 메서드 인지 컴파일러를 통해 확인이 어려움
스멜
상속보다는 컴포지션이 더 좋다 <<GoF의 디자인 패턴>>
리팩터링: 삭제 후 컴파일하기
인터페이스에서 사용하지 않는 메서드를 제거하는 것
메서드를 삭제하고 컴파일러에서 허용하는지 확인하는 것
5장. 유사한 코드 융합하기
리팩터링: 유사 클래스 통합
일련의 상수 메서드를 공통으로 가진 두 개 이상의 클래스에서 일련의 상수 메서드가 클래스에 따라 다른 값을 반환할 때 클래스를 통합할 수 있다.
리펙터링: if 문 결합
동일한 연속적인 if 문을 결합해서 중복을 제거
규칙: 순수 조건 사용
조건에 부수적인 동작이 없음을 의미
부수적인 동작: 조건이 변수에 값을 할당하거나 예외를 발생시키거나, 출력, 파일 쓰기 등과 같은 I/O 와 상호작용 하는 것을 의미
스멜
명령에서 질의 분리
명령은 부작용이 있는 모든 것을 의미, 질의는 순수한 것을 의미
void 메서드에서는 부수적인 동작을 허용, 즉 부수적인 동작을 하거나 or 값을 반환하거나 둘 중 하나만 하는 형태
의도
데이터를 가져오는 것과 변경하는 것을 분리하는 것 -> 코드를 깔끔하고 예측 가능하게 만들어줌
클래스 다이어그램
인터페이스와 클래스의 구조가 서로 어떤 관계가 있는지 보여줌
클래스의 공용 인터페이스에 관심이 있어서, 비공개 항목은 포함하지 않음
클래스와 인터페이스의 관계
- X가 Y를 사용한다. (Uses a) -> 의존, 연관
- X는 Y다. (is a) -> 상속, 구현
- X가 하나 또는 여러개의 Y를 가진다. (Has a) -> 집합, 컴포지션
인터페이스에서만 상속받을 것이라는 규칙으로 인해 상속은 사용 X, 의존, 연관 관계는 관계가 무엇인지 모르거나 신경쓰지 않을때 사용, 컴포지션과, 집합의 차이는 어떻게 표현하는가의 문제 (동일)
따라서 대부분 컴포지션과 구현, 두 가지 관계 유형을 사용
리팩터링: 전략 패턴의 도입
전략 패턴: 다른 클래스를 인스턴스화해서 변형을 도입하는 개념
전략 패턴을 도입하는 상황
- 코드에 변형을 도입하고 싶어서 리펙터링을 수행하는 경우
- 떨어지는 성질을 코드화했던 상황에서 바로 변형의 추가가 필요하다고 예상하지 않았을 때
10장. 코드 추가에 대한 두려움 떨쳐내기
불확실성 받아들이기: 위험 감수
겁을 먹으면 효과적으로 일할 수 없다. 소프트웨어 개발은 도메인을 학습하고 그 지식을 프로그래밍 언어로 코드화 하는 것
그 과정중에는 용기가 필요하다.
우리는 불확실한 영역을 두려워할 때가 많지만 그 영역이 우리가 배워야 할 부분
위험에 뛰어들라
두려움은 심리적 고통의 한 형태, 뭔가 두렵다면 더 이상 두렵지 않을 때까지 더 시도
스파이크를 사용한 두려움 극복
스파이크: 프로젝트 초기에 스토리를 진행하기 앞서 사용할 기술(라이브러리, 프레임워크)을 조사하고, 요구사항에 관련된 배경지식을 습득하는 단계
스파이크가 제품의 코드나 기능이 아닌 지식이라는 것을 인지할 필요가 있음
스파이크가 제품의 코드라는 생각이 들면, 프로덕션을 위한 코드를 작성하는 것과 같은 두려움이 커질 수 있음
스파이크를 통해 얻은 지식을 문서화하여 이해관계자, 팀원들과 공유하는 것이 중요
두려움 극복을 위한 사용 시간 비율 지정
프로적션 코드와 지원 도구 사이의 합리적인 복잡성 비율 80:20
이해관계자의 요청에 의해 발생되지 않는 모든 작업을 금요일에 몰아서 하는 방법도 효율적
금요일에는 주로 실험하거나, 리펙토링, 개발 작업을 자동화하는 작업을 수행
점진적 개선
가면 증후군: 스스로를 자격이 없는 사람으로 간주하여 누군가 자신을 사기꾼으로 폭로할까 봐 두려워하는 것
개발자들은 종종 다른 사람의 코드를 경솔하게 비판 -> 이는 가면 증후군을 불러일으킴
완벽한 코드는 존재하지 않음. 무엇에 초점을 맞출 것인지, 그리고 어디에서 불완전함을 받아들일 것인지 선택해야함
복제된 코드가 속도에 미치는 영향
코드를 공유하면 코드가 사용되는 모든 위치에 영향을 미치기 쉽다.
코드를 공유하면 전역적인 동작 변경 속도가 증가하고, 코드를 복제하면 지역적인 동작 변경 속도가 증가한다.
전역적인 동작 변경 속도가 높다는 것은 -> 코드상으로 관련이 없어 보이는 부분에도 영향을 미친다는 것: 취약성
"이것을 원본과 결합해야 하는가?"
"복사본이 바뀌면 원본이 바뀌어야 하는가?"
"팀이 통합된 코드를 가지고 있는가?"
위 질문에 답이 "아니요" 인 경우에는 복제되어 관리되는 것이 좋음
확장성을 통한 추가에 의한 변경
모든 것을 확장가능하게 만ㄷ그는 것은 코드를 불필요하게 복잡하게 만듦
우발적 복잡성: 도메인에 관련되지 않은 복잡성
본질적 복잡성: 코드가 실제 세계를 나타내면서 상속된 일부 복잡성
우발적 복잡성을 없애기 위한 세가지 단계
- 코드를 복사
- 복사본을 이용해 작업
- 합리적이라는 판단하에 원본과 통합
리펙토링 패턴 중 확장-수축 패턴과 유사
추가에 의한 변경으로 이전 버전과의 호환성 확보
이전 기능들이 동작하는 형태로 새로운 기능 추가
기능 플래그를 통한 변경
기능플래그를 통해 지속적으로 프로덕션에 나가는 환경 구성
프로적션에 100% 서빙된 기능 플래그는 제거해야함
추상화를 통한 분기: 기능플래그가 true, false 가 아닌 NewA, OldA 같은 것으로 반환되게 하는 것
11장. 코드 구조 따르기
범위와 출처에 따른 구조 분류
팀 간 | 팀 내 | |
---|---|---|
코드에 있는 경우 | 외부 API | 데이터와 함수, 대부분의 리펙토링 |
사람에 있는 경우 | 조직도, 프로세스 | 행위 및 도메인 전문가 |
조직 구성과 그 조직이 만든 시스템 구조는 닮는 경향이 있다. -> 콘웨이 법칙
행위를 코드화하는 세가지 방법
- 제어 흐름
- 데이터 구조
- 데이터 자체
제어 흐름에 행위 코드화하기
제어 흐름은 제어 연산자. 메서드 호출, 또는 열거된 코드의 줄을 통해 행위를 텍스트로 표현
제어 흐흠 안에 행위를 기술하면 커다란 변화를 만들기 쉬움
안정성과 작은 변화를 선호하기 때문에 제어 흐름을 사용해 리펙터링하지 않음
그러나 어떤 상황에서는 큰 조정이 필요한 경우가 있기 때문에 제어 흐름으로 리펙터링 후, 다시 리펙터링 하는 것도 유용할 수 있음
데이터 구조에 행위 코드화 하기
데이터 구조에 행위 코드화 변형점과 일치하지 않는 한 큰 변경을 수행하기 어려움. 그러나 작은 변경은 쉽고 안전함
클래스로 타입 코드 대체, 전략 패턴 도입 리펙터링 패턴 모두 제어 흐름을 데이터 구조로 옮긴 것
데이터에 행위 코드화 하기
컴파일러의 지원을 받지 못하기 때문에 안전하게 사용하는 것이 어려움, 비추
구조 노출을 위한 코드 추가
우리는 소프트웨어를 올바르다고 믿는 특정한 방향으로 변경하기 위해 리펙터링을 사용
주변에 참고할 코드가 많을 수록 이 방향의 코드와 데이터가 많이 있기 때문에 코드가 어떻게 변경될지 알 가능성이 높음
기본 구조에 대한 확신이 없다면 리펙터링 노력을 줄이고 정확성에 집중해야 함
새로운 기능이나 하위 시스템을 구현할 때는 불확실성이 있기 때문에 이럴때는 클래스보다 열거형이나, 루프를 사용하는 것이 좋음
즉 상황에 맞춰서 하는 것이 좋다는 뜻
예측 대신 관찰, 그리고 경험적 기술 사용
예측하려는 시도는 코드베이스레 도움이 되기보다는 손상을 줄수 있음
코드를 추측하지 말고 경험적인 기술을 사용해야함
- 변경되지 않으면 아무것도 하지 마십시오.
- 예측할 수 없이 변경되는 경우 취약성을 피하기 위해서만 리펙터링하시오.
- 그렇지 않으면, 과거에 발생한 변경 유형을 적용해 리펙터링하시오.
코드를 이해하지 않고도 안정성을 확보하는 방안
이미 코드에 있는ㄴ 구조를 따르고 실수 없는 믿을만한 리펙터링 패턴을 사용하는 한 작업을 위해 코드를 이해할 필요 없음
- 테스트를 통한 안정성 확보
- 숙달을 통한 안정성 확보
- 도구의 지원을 통한 안정성 확보
- 공식 인증을 통한 안정성 확보
- 내결함성을 통한 안정성 확보
활용되지 않은 구조 이용
우연이거나 일시적인 구조를 이용하는 것은 속도 저하로 이어질 수 있음
도메인으로부터 온 구조는 흔히 안전하게 활용할 수 있음
구조가 활용 가치가 있는지를 결정하기 전에 먼저 구조를 찾아야 함
추출 및 캡슐화에 공백 활용
개발자는 머릿속에서 그룹화하기 때문에 빈 줄을 사용해서 지각된 구조를 표현할 때가 많음
사이에 공백을 넣어 그룹화된 되어 있는 경우 -> 메서드 추출 패턴을 생각해봐야 함
공백의 일반적인 위치는 필드를 그룹화하는 데 사용되는 경우 -> 데이터 캡슐화 리펙터링 패턴을 생각해봐야 함
통합에 중복 코드 활용
코드 중복에 대해 통합을 적용 시킬 수 있음
- 메서드 추출
- 메서드 캡슐화
- 유사 클래스 통합
- 전략 패턴 도입
리펙토링 패턴을 이용하면 구조가 숨겨져 있는 경우에도 해당 구조를 노출할 수 있음
캡슐화로 공통 접사 활용
공백이나 중복, 또는 이름의 공통적인 명칭을 통해 그룹화된 것을 발견할 때 이 구조를 견고하게 하는 방법은 데이터 캡슐화
유사한 이름을 가진 클래스들을 그룹화하는 데도 해당 규칙이 사용될 수 있음
언어만다 지원하는 방식이 다르지만, TS 에서는 namespace 모듈을 사용함
동적 실행으로 런타임 유형 활용
객체지향 프로그래밍은 인터페이스를 통한 동적 실행이라는 강력한 매커니즘이 내장되어 있기 때문에 런타임 타입을 검사하는 기능 업싱 고안
두 가지 타입 (A, B)을 가질 수 있는 변수가 있다고 가정했을때, 보통은 현재 타입을 직접 검사해서 적절한 동작을 수행
- 만약 A 와 B 를 통제할 수 있는 경우
- 이러한 경우에는 클래스로 코드 이관을 통해서 새로운 인터페이스를 만들고, 변수를 이 타입을 가지도록 변경한 후 두 클래스가 모두 인터페이스를 구현하도록 함
- 만약 A 와 B 를 통제할 수 없는 경우
- 코드베이스가 오염되지 않도록 타입 검사를 코드의 가장자리에 위치